﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Application.Controls
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.Windows.Forms;

    /// <summary>Our customized tree view.</summary>
    /// <threadsafety static="true" instance="false"/>
    public class CustomTreeView : TreeView
    {
        private readonly Action<EventArgs> onNodeTap;
        private ContextMenu contextMenu;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="CustomTreeView"/> class.</summary>
        public CustomTreeView()
        {
            this.onNodeTap = this.OnNodeTap;
            WndProcHooker.HookWndProc(this, this.WM_LBUTTONDOWN_Handler, NativeMethods.WM_LBUTTONDOWN);
            WndProcHooker.HookWndProc(this, this.WM_CONTEXTMENU_Handler, NativeMethods.WM_CONTEXTMENU);
        }

        /// <summary>See <see cref="Control.ContextMenu"/>.</summary>
        /// <remarks>The base class implementation of this property is such that a tap &amp; hold shows the
        /// set context menu but there's no longer any way to distinguish tap &amp; hold from a tap. This override
        /// stores the set context menu in its own private field. The menu is still shown whenever there is a
        /// tap &amp; hold gesture on a tree node, but client code can now use the <see cref="NodeTap"/> event to
        /// distinguish tap from tap &amp; hold.</remarks>
        public override ContextMenu ContextMenu
        {
            get
            {
                return this.contextMenu;
            }

            set
            {
                this.contextMenu = value;
            }
        }

        /// <summary>Occurs when a tree node is tapped.</summary>
        /// <remarks>Does not occur with a tap &amp; hold gesture. Always occurs after the
        /// <see cref="TreeView.AfterSelect"/> event.</remarks>
        public event EventHandler<EventArgs> NodeTap;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Raises the <see cref="NodeTap"/> event.</summary>
        protected virtual void OnNodeTap(EventArgs e)
        {
            if (this.NodeTap != null)
            {
                this.NodeTap(this, e);
            }
        }

        /// <summary>See <see cref="Control.OnKeyDown"/>.</summary>
        /// <remarks>Filters action button and enter key down such that the <see cref="Control.KeyDown"/> event is only
        /// raised if a node of the tree is currently selected and visible. Moreover, if the action button is pressed
        /// long enough for a "tap &amp; hold" gesture and <see cref="ContextMenu"/> is not <c>null</c> then the context
        /// menu is shown.</remarks>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if ((e.KeyCode == Keys.Enter) && (this.SelectedNode != null))
            {
                var nativeRect = new NativeMethods.RECT();
                nativeRect.Left = this.SelectedNode.Handle.ToInt32();
                IntPtr handle = this.Handle;

                if (NativeMethods.SendMessage(handle, NativeMethods.TVM_GETITEMRECT.MSG, 1, ref nativeRect) == 0)
                {
                    // Contrary to the documentation, TVM_GETITEMRECT does not return 0 when the node is not visible,
                    // but we have to have the code in place in case this is corrected in a future version.
                    e.Handled = true;
                    return;
                }

                var nodeRectangle = new Rectangle(
                    nativeRect.Left,
                    nativeRect.Top,
                    nativeRect.Right - nativeRect.Left,
                    nativeRect.Bottom - nativeRect.Top);

                if (!nodeRectangle.IntersectsWith(this.ClientRectangle))
                {
                    // The node is not visible -> don't raise the KeyDown event
                    e.Handled = true;
                    return;
                }

                nodeRectangle.Intersect(this.ClientRectangle);
                Point center = new Point(
                    (nodeRectangle.Right + nodeRectangle.Left) / 2,
                    (nodeRectangle.Bottom + nodeRectangle.Top) / 2);

                if (this.RecognizeTapAndHold(handle, center))
                {
                    // We recognized a tap & hold -> don't raise the KeyDown event
                    e.Handled = true;
                    return;
                }
            }

            base.OnKeyDown(e);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "wParam is an official name")]
        private int WM_LBUTTONDOWN_Handler(IntPtr hwnd, uint msg, uint wParam, int lParam, ref bool handled)
        {
            if ((wParam & NativeMethods.MK_LBUTTON) == NativeMethods.MK_LBUTTON)
            {
                var position = NativeMethods.LParamToPoint(lParam);
                NativeMethods.TVHITTESTINFO hti = new NativeMethods.TVHITTESTINFO(position.X, position.Y);

                if ((NativeMethods.SendMessage(hwnd, NativeMethods.TVM_HITTEST.MSG, 0, ref hti) != 0) &&
                    ((hti.flags & NativeMethods.TVHT.ONITEM) != 0))
                {
                    if (NativeMethods.SendMessage(
                            hwnd, NativeMethods.TVM_SELECTITEM.MSG, NativeMethods.TVGN.CARET, hti.hItem) &&
                        !this.RecognizeTapAndHold(hwnd, position))
                    {
                        // This ensures that the node is selected before the event is fired. This allows client code
                        // to easily determine which node was selected.
                        this.BeginInvoke(this.onNodeTap, EventArgs.Empty);
                    }
                }
            }

            return 0;
        }

        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "wParam is an official name")]
        private int WM_CONTEXTMENU_Handler(IntPtr hwnd, uint msg, uint wParam, int lParam, ref bool handled)
        {
            this.contextMenu.Show(this, this.PointToClient(NativeMethods.LParamToPoint(lParam)));
            return 0;
        }

        private bool RecognizeTapAndHold(IntPtr hwnd, Point position)
        {
            return (this.contextMenu != null) &&
                (NativeMethods.SHRecognizeGesture(new NativeMethods.SHRGINFO(hwnd, position.X, position.Y)) != 0);
        }
    }
}
